﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace GameDataTempshitEditor.Widgets
{
    public partial class DataObjectTree : UserControl
    {
        public DataObjectTree(Red.GameData.EditorEnvironment env, Red.GameData.Raw.ReflectionClass rootClass)
        {
            InitializeComponent();

            _Env = env;
            _RootClass = rootClass;

            RefreshDataTree();
        }

        private Red.GameData.EditorEnvironment _Env;
        private Red.GameData.Raw.ReflectionClass _RootClass;

        public Red.GameData.Raw.ReflectionClass RootClass
        {
            get
            {
                return _RootClass;
            }
        }

        public Red.GameData.Raw.DataObjectView SelectedDataView
        {
            get
            {
                return GetSelectedDataObject();
            }
            set
            {
                SelectDataObject((value != null) ? value.RawData : null);
            }
        }

        public Red.GameData.Raw.DataObject SelectedDataObject
        {
            get
            {
                var ret = GetSelectedDataObject();
                return (ret != null) ? ret.RawData : null;
            }
            set
            {
                SelectDataObject(value);
            }
        }

        //---------------------

        public delegate void DataObjectHandler(object sender, DataObjectEventArgs e);
        public event DataObjectHandler OnObjectCreated;
        public event DataObjectHandler OnObjectSelected;
        public event DataObjectHandler OnObjectDeleted;
        public event DataObjectHandler OnObjectActivated;

        //---------------------

        private class TreeNode_DataObjectView : TreeNode
        {
            public readonly Red.GameData.Raw.DataObjectView _View;

            public TreeNode_DataObjectView(Font font, Red.GameData.Raw.DataObjectView view)
                : base(view.Name)
            {
                _View = view;

                // apply stylization
                if (_View.RawData.IsAbstract)
                    font = new Font(font, font.Style | FontStyle.Italic);
                    
                // child classes
                foreach (var childObject in _View.Children)
                    Nodes.Add(new TreeNode_DataObjectView(font, childObject));
            }

            public void RefreshName()
            {
                Text = _View.Name; // reevaluate

                foreach (TreeNode_DataObjectView childObject in Nodes)
                    childObject.RefreshName();
            }
        }

        public void RefreshNames()
        {
            try
            {
                foreach ( TreeNode_DataObjectView node in dataTree.Nodes )
                {
                    node.RefreshName();
                }
            }
            catch ( Exception )
            {}
        }

        private void RefreshDataTree()
        {
            var currentFilter = dataFilterText.Text;

            dataTree.BeginUpdate();
            dataTree.Nodes.Clear();

            // get ALL objects of this class, NOTE: very slow for now, requires a tedious resolve for each data object
            // TODO: well, optimize
            if ( _RootClass != null)
            {
                var rootObjects = _Env.Data.ExtractNamedObjects(_RootClass.Name, _RootClass.KeyName, _Env.Reflection);
                foreach (var rootNode in rootObjects)
                {
                    dataTree.Nodes.Add(new TreeNode_DataObjectView(Font, rootNode));
                }
            }

            // finish update
            dataTree.EndUpdate();
            dataTree.Refresh();
        }

        private TreeNode FindDataTreeNode(Red.GameData.Raw.DataObject obj, TreeNode_DataObjectView parent = null )        
        {
            if (parent == null)
            {
                foreach (var node in dataTree.Nodes)
                {
                    var ret = FindDataTreeNode(obj, (TreeNode_DataObjectView)node);
                    if (ret != null)
                        return ret;
                }
            }
            else
            {
                if ((parent._View != null) && (parent._View.RawData == obj))
                    return parent;

                foreach (var node in parent.Nodes)
                {
                    var ret = FindDataTreeNode(obj, (TreeNode_DataObjectView)node);
                    if (ret != null)
                        return ret;
                }
            }

            return null;
        }

        private TreeNode SelectDataObject( Red.GameData.Raw.DataObject obj )
        {
            var node = FindDataTreeNode(obj);
            if (node != null)
            {
                dataTree.SelectedNode = node;
                return node;
            }

            return null;
        }

        private Red.GameData.Raw.DataObjectView GetSelectedDataObject()
        {
            try
            {
                var node = (TreeNode_DataObjectView)dataTree.SelectedNode;
                if (node != null)
                    return node._View;
            }
            catch (Exception)
            {
            }

            return null;
        }

        private void RefreshSingleObject(Red.GameData.Raw.DataObjectView objectView)
        {
            // find the tree node matching the parent
            var parentNode = FindDataTreeNode((objectView.Parent != null) ? objectView.Parent.RawData : null);

            // something failed - refresh the whole tree
            if (objectView.Parent != null && parentNode == null)
            {
                RefreshDataTree();
            }
            else
            {
                // create just the new node
                var newNode = new TreeNode_DataObjectView(dataTree.Font, objectView);
                if (parentNode != null)
                    parentNode.Nodes.Add(newNode);
                else
                    dataTree.Nodes.Add(newNode);
            }

            // select object
            SelectDataObject(objectView.RawData);
        }

        //--------------------------------------------------------------------------------------------------------------

        public Red.GameData.Raw.DataObjectView CreateNewRootObject( Red.GameData.Raw.ReflectionClass rawClass )
        {
            return CreateNewObject(null, rawClass);
        }

        private Red.GameData.Raw.DataObjectView CreateNewObject(Red.GameData.Raw.DataObjectView targetObject, Red.GameData.Raw.ReflectionClass rawClass, bool autoEditName = true)
        {
            // validate class
            if (rawClass == null || _Env == null)
            {
                ErrorBox.ShowError("No class selected. Please select class first");
                return null;
            }

            // unnamed class
            if (!rawClass.IsNamed)
            {
                ErrorBox.ShowError(String.Format("Class {0} does not have a \"key\" proerty defined and the created objects would be nameless. Unable to proceed.", rawClass.Name));
                return null;
            }

            // abstract class
            if (rawClass.IsAbstract)
            {
                ErrorBox.ShowError(String.Format("Class {0} is abstract and cannot be used to create data objects. Use derived class.", rawClass.Name));
                return null;
            }

            // if we are adding new derived object please make sure that the class is allowed
            Red.GameData.Raw.DataObjectView newObject = null;
            if (targetObject != null)
            {
                var objClass = _Env.Reflection.FindClass(targetObject.ClassName);
                if (objClass != null && !rawClass.IsA(objClass))
                {
                    ErrorBox.ShowError(String.Format("Class '{0}' does not extend class '{1}' used in object '{2}'. Unable to create new object.",
                        rawClass.Name, targetObject.ClassName, targetObject.Name));
                    return null;
                }

                // derive existing object
                newObject = _Env.Data.Action_DeriveObject(null, targetObject, rawClass.Name);
            }
            else
            {
                // add new object
                // TODO: file picker ?
                Red.GameData.Raw.DataFile suggestedFile = null;
                newObject = _Env.Data.Action_NewRootObjectView(_Env.Reflection, suggestedFile, rawClass.Name, "");
            }

            // whoops, no object
            if (newObject == null)
            {
                ErrorBox.ShowError("Unable to create new object!");
                return null;
            }

            // refresh the list
            RefreshSingleObject(newObject);

            // callback
            if (OnObjectCreated != null)
                OnObjectCreated(this, new DataObjectEventArgs(newObject));

            // start name edition
            if (autoEditName)
            {
                var treeNode = FindDataTreeNode(newObject.RawData);
                if (treeNode != null)
                {
                    treeNode.BeginEdit();
                }
            }

            return newObject;
        }

        private Red.GameData.Raw.DataObjectView DeriveObject(Red.GameData.Raw.DataObjectView targetObject, bool autoEditName = true)
        {
            // if we are adding new derived object please make sure that the class is allowed
            Red.GameData.Raw.DataObjectView newObject = _Env.Data.Action_DeriveObject(null, targetObject);
            if (newObject == null)
            {
                ErrorBox.ShowError("Unable to create new object!");
                return null;
            }

            // refresh the list
            RefreshSingleObject(newObject);

            // callback
            if (OnObjectCreated != null)
                OnObjectCreated(this, new DataObjectEventArgs(newObject));

            // start name edition
            if (autoEditName)
            {
                var treeNode = FindDataTreeNode(newObject.RawData);
                if (treeNode != null)
                {
                    treeNode.BeginEdit();
                }
            }

            return newObject;
        }

        private Red.GameData.Raw.DataObjectView DuplicateObject(Red.GameData.Raw.DataObjectView targetObject, bool autoEditName = true)
        {
            // if we are adding new derived object please make sure that the class is allowed
            Red.GameData.Raw.DataObjectView newObject = _Env.Data.Action_CopyObject(null, targetObject);
            if (newObject == null)
            {
                ErrorBox.ShowError("Unable to create new object!");
                return null;
            }

            // refresh the list
            RefreshSingleObject(newObject);

            // callback
            if (OnObjectCreated != null)
                OnObjectCreated(this, new DataObjectEventArgs(newObject));

            // start name edition
            if (autoEditName)
            {
                var treeNode = FindDataTreeNode(newObject.RawData);
                if (treeNode != null)
                {
                    treeNode.BeginEdit();
                }
            }

            return newObject;
        }

        //--------------------------------------------------------------------------------------------------------------

        private void dataFilterText_TextChanged(object sender, EventArgs e)
        {
            timer1.Interval = 100;
            timer1.Stop();
            timer1.Start();
        }

        private void timer1_Tick(object sender, EventArgs e)
        {
            RefreshDataTree();
        }


        private Red.GameData.Raw.ReflectionClass GetDraggedClass(IDataObject obj)
        {
            try
            {
                return (Red.GameData.Raw.ReflectionClass)obj.GetData(typeof(Red.GameData.Raw.ReflectionClass));
            }
            catch (Exception) { };

            return null;
        }

        private void dataTree_DragDrop(object sender, DragEventArgs e)
        {
            try
            {
                // Retrieve the client coordinates of the drop location.
                Point targetPoint = dataTree.PointToClient(new Point(e.X, e.Y));
                var targetDataNode = (TreeNode_DataObjectView)dataTree.GetNodeAt(targetPoint);
                var targetData = (targetDataNode != null) ? targetDataNode._View : null;

                // Retrieve the node that was dragged.
                var draggedClass = GetDraggedClass(e.Data);
                if (draggedClass != null)
                {
                    // Create new object from the dragged class
                    CreateNewObject(targetData, draggedClass);
                }

                // TODO: object dragging - reparent?
            }
            catch (Exception)
            {
            }
        }

        private void dataTree_DragEnter(object sender, DragEventArgs e)
        {
            // only allow rooted classes
            var draggedClass = GetDraggedClass(e.Data);
            if (draggedClass != null && draggedClass.IsA(_RootClass))
            {
                e.Effect = e.AllowedEffect;
            }
        }

        private void dataTree_DragLeave(object sender, EventArgs e)
        {

        }

        private void dataTree_DragOver(object sender, DragEventArgs e)
        {
            Point targetPoint = dataTree.PointToClient(new Point(e.X, e.Y));
            dataTree.SelectedNode = dataTree.GetNodeAt(targetPoint);
        }

        private void ShowContextMenuFor(Red.GameData.Raw.DataObjectView obj)
        {
            var dataObject = GetSelectedDataObject();

            if (dataObject != null)
            {
                deriveToolStripMenuItem.Visible = true;
                duplicateToolStripMenuItem.Visible = true;
                deleteToolStripMenuItem.Visible = true;
            }
            else
            {
                duplicateToolStripMenuItem.Visible = false;
                deriveToolStripMenuItem.Visible = false;
                deleteToolStripMenuItem.Visible = false;
            }

            // don't show if there's nothing visible
            bool hasVisibleItems = false;
            foreach (var item in contextMenuStrip1.Items)
            {
                try
                {
                    var menuItem = (ToolStripMenuItem)item;
                    hasVisibleItems |= menuItem.Visible;
                }
                catch (InvalidCastException) { }
                catch (NullReferenceException) { }
            }

            // no items visible
            if (!hasVisibleItems)
                return;

            // global show
            contextMenuStrip1.Show(System.Windows.Forms.Cursor.Position);
        }

 
        private void duplicateToolStripMenuItem_Click(object sender, EventArgs e)
        {
            DeriveObject(GetSelectedDataObject());
        }

        private void deriveToolStripMenuItem_Click(object sender, EventArgs e)
        {
            DeriveObject(GetSelectedDataObject());
        }

        private void deleteToolStripMenuItem_Click(object sender, EventArgs e)
        {

        }

        private void dataTree_MouseUp(object sender, MouseEventArgs e)
        {
            if (e.Button == System.Windows.Forms.MouseButtons.Right)
            {
                try
                {
                    Point targetPoint = dataTree.PointToClient(new Point(e.X, e.Y));
                    var node = (TreeNode_DataObjectView)dataTree.GetNodeAt(targetPoint);
                    if (node != null)
                        ShowContextMenuFor(node._View);
                }
                catch (InvalidCastException) { };
            }
        }

        private void dataTree_MouseDoubleClick(object sender, MouseEventArgs e)
        {
            if (OnObjectActivated != null)
                OnObjectActivated(this, new DataObjectEventArgs(SelectedDataView));
        }

        private void dataTree_AfterSelect(object sender, TreeViewEventArgs e)
        {
            if (OnObjectSelected != null)
                OnObjectSelected(this, new DataObjectEventArgs(SelectedDataView));
        }

        private void dataTree_AfterLabelEdit(object sender, NodeLabelEditEventArgs e)
        {
            if (e.Label != null && e.Label.Length > 1)
            {
                var node = (TreeNode_DataObjectView)e.Node;
                node._View.Name = e.Label;
            }
            else
            {
                e.CancelEdit = true;
            }
        }

        private void DeleteSelected()
        {
            var selection = SelectedDataView;
            if (selection != null)
            {
                // inform the editor before the actual object will be deleted
                if (OnObjectDeleted != null)
                    OnObjectDeleted(this, new DataObjectEventArgs(selection));

                // delete the object - NOTE this may fail
                string errorMessage = "";
                if (!_Env.Data.Action_DeleteObject(selection.RawData, ref errorMessage))
                {
                    ErrorBox.ShowError("Error deleting object:\r\n" + errorMessage);
                }
                else
                {
                    // remove the node
                    if (dataTree.SelectedNode != null)
                    {
                        dataTree.SelectedNode.Remove();
                        dataTree.Refresh();
                    }
                }
            }
        }

        private void button1_Click(object sender, EventArgs e)
        {
            DeleteSelected();
        }

        private void dataTree_KeyDown(object sender, KeyEventArgs e)
        {
            var selection = SelectedDataView;
            if (e.KeyCode == Keys.Enter && selection != null)
            {
                if (OnObjectActivated != null)
                    OnObjectActivated(this, new DataObjectEventArgs(selection));
            }
            else if (e.KeyCode == Keys.Delete && selection != null)
            {
                DeleteSelected();
            }
            else if (e.KeyCode == Keys.F2 && selection != null)
            {
                if (null != dataTree.SelectedNode)
                    dataTree.SelectedNode.BeginEdit();
            }
        }

        private void button2_Click(object sender, EventArgs e)
        {
            var selection = SelectedDataView;
            if (selection != null)
            {
                DuplicateObject(selection);
            }            
        }       

    }

    public class DataObjectEventArgs : EventArgs
    {
        public Red.GameData.Raw.DataObjectView ObjectView { get; private set; }

        public DataObjectEventArgs(Red.GameData.Raw.DataObjectView view)
        {
            ObjectView = view;
        }
    }

    public interface IDataObjectProvider
    {
        Red.GameData.Raw.DataObjectView AsObjectView();
    }
}
